package yyy.ab.logging.aspect;


import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import yyy.ab.common.properties.AbProperties;
import yyy.ab.common.utils.RequestHolder;
import yyy.ab.common.utils.SecurityUtils;
import yyy.ab.common.utils.StringUtils;
import yyy.ab.common.utils.ThrowableUtil;
import yyy.ab.logging.entity.SysLog;
import yyy.ab.logging.service.SysLogService;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.*;

@Slf4j
@Aspect
@Configuration
public class LogRecordAspect {

    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private AbProperties abProperties;
    @Autowired
    private SysLogService logService;

    ThreadLocal<Long> currentTime = new ThreadLocal<>();

    /**
     * 定义存储日志切入点
     */
    @Pointcut("@annotation(yyy.ab.logging.annotation.Log)")
    public void logPointcut() {
        // 该方法无方法体,主要为了让同类中其他方法使用此切入点
    }

    /**
     * 定义打印请求切入点.
     * 解释下：
     * ~ 第一个 * 代表任意修饰符及任意返回值.
     * ~ 第二个 * 任意包名
     * ~ 第三个 * 代表任意方法.
     * ~ 第四个 * 定义在controller包或者子包
     * ~ 第五个 * 任意方法
     * ~ .. 匹配任意数量的参数.
     */
    @Pointcut("execution(* yyy.ab..*.controller..*.*(..))")
    public void printPointcut() {
    }

    /**
     * 在切点之前织入
     *
     * @param joinPoint
     * @throws Throwable
     */
    @Before("printPointcut()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 开始打印请求日志
        HttpServletRequest request = RequestHolder.getHttpServletRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Object[] args = joinPoint.getArgs();
        StringBuilder params = getParams(method, args);

        // 打印请求相关参数
        log.info("========================================== Start ==========================================");
        // 打印请求 url
        log.info("URL            : {}", request.getRequestURL().toString());
        // 打印 Http method
        log.info("HTTP Method    : {}", request.getMethod());
        // 打印调用 controller 的全路径以及执行方法
        log.info("Class Method   : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
        // 打印请求的 IP
        // log.info("IP             : {}", request.getRemoteAddr());
        // 打印请求入参
        log.info("Request Args   : {}", params.toString());
    }

    /**
     * 在切点之后织入
     *
     * @throws Throwable
     */
    @After("printPointcut()")
    public void doAfter() throws Throwable {
        log.info("=========================================== End ===========================================");
        // 每个请求之间空一行
        log.info("");
    }

    /**
     * 环绕日志打印
     *
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("printPointcut()")
    public Object doPrintAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        currentTime.set(System.currentTimeMillis());
        Object result = proceedingJoinPoint.proceed();
        currentTime.remove();
        return result;
    }

    /**
     * 环绕===记录日志
     *
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("logPointcut()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = proceedingJoinPoint.proceed();
        long time = System.currentTimeMillis() - startTime;
        if (abProperties.isOpenAopLog()) {
            SysLog sysLog = new SysLog("INFO", time);
            HttpServletRequest request = RequestHolder.getHttpServletRequest();
            // 开始打印请求日志
            MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
            Method method = signature.getMethod();
            Object[] args = proceedingJoinPoint.getArgs();
            StringBuilder params = getParams(method, args);
            logService.save(getUsername(), StringUtils.getBrowser(request), StringUtils.getIp(request), proceedingJoinPoint, params, sysLog);
        }

        return result;
    }

    /**
     * 配置异常通知
     *
     * @param joinPoint join point for advice
     * @param e         exception
     */
    @AfterThrowing(pointcut = "printPointcut()", throwing = "e")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) throws IOException {
        if (abProperties.isOpenAopLog()) {
            SysLog sysLog = new SysLog("ERROR", System.currentTimeMillis() - currentTime.get());
            currentTime.remove();
            sysLog.setExceptionType(e.getClass().getName());
            sysLog.setExceptionDetail(ThrowableUtil.getStackTrace(e));
            HttpServletRequest request = RequestHolder.getHttpServletRequest();
            // 开始打印请求日志
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            Object[] args = joinPoint.getArgs();
            StringBuilder params = getParams(method, args);
            logService.save(getUsername(), StringUtils.getBrowser(request), StringUtils.getIp(request), (ProceedingJoinPoint) joinPoint, params, sysLog);
        }
    }

    /* 获取请求的参数 */
    private StringBuilder getParams(Method method, Object[] args) throws IOException {
        StringBuilder params = new StringBuilder();
        // 请求的方法参数名称
        LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
        String[] paramNames = u.getParameterNames(method);
        if (args != null && paramNames != null) {
            params = handleParams(params, args, Arrays.asList(paramNames));
        }
        return params;
    }

    /**
     * 获取请求参数的字符串
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private StringBuilder handleParams(StringBuilder params, Object[] args, List paramNames) throws IOException {
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof Map) {
                Set set = ((Map) args[i]).keySet();
                List list = new ArrayList();
                List paramList = new ArrayList<>();
                for (Object key : set) {
                    list.add(((Map) args[i]).get(key));
                    paramList.add(key);
                }
                return handleParams(params, list.toArray(), paramList);

            } else {
                if (args[i] instanceof Serializable) {
                    Class<?> aClass = args[i].getClass();
                    try {
                        aClass.getDeclaredMethod("toString", new Class[]{null});
                        // 如果不抛出 NoSuchMethodException 异常则存在 toString 方法 ，安全的 writeValueAsString ，否则 走 Object的 toString方法
                        params.append("  ").append(paramNames.get(i)).append(": ").append(objectMapper.writeValueAsString(args[i]));
                    } catch (NoSuchMethodException e) {
                        params.append("  ").append(paramNames.get(i)).append(": ").append(objectMapper.writeValueAsString(args[i].toString()));
                    }

                } else if (args[i] instanceof MultipartFile) {
                    MultipartFile file = (MultipartFile) args[i];
                    params.append("  ").append(paramNames.get(i)).append(": ").append(file.getName());
                } else {
                    params.append("  ").append(paramNames.get(i)).append(": ").append(args[i]);
                }
            }
        }
        return params;
    }

    public String getUsername() {
        try {
            return SecurityUtils.getCurrentUsername();
        } catch (Exception e) {
            return "";
        }
    }
}
